package com.adayo.ble;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.SystemClock;


import com.adayo.ble.utils.JLogKit;

import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Service for managing connection and data communication with a GATT server
 * hosted on a given Bluetooth LE device.
 * <p/>
 * Created by Billin on 2017/5/12.
 */
public class BluetoothLeConnector {

	/**
	 * 连接状态回调
	 */
	public interface OnConnectListener {
		void onConnect();

		void onDisconnect();

		void onServiceDiscover();

		void onError(String msg);
	}

	/**
	 * 读写回调接口
	 */
	public interface OnDataAvailableListener {
		void onCharacteristicRead(byte[] values, int status);

		void onCharacteristicChange(UUID characteristic, byte[] values);

		void onCharacteristicWrite(UUID characteristic, int status);

		void onDescriptorWrite(UUID descriptor, int status);

		void onError(String msg);
	}
	private static final UUID UUID_SERVICE_CHANNEL
			= UUID.fromString("49535343-fe7d-4ae5-8fa9-9fafd205e455");
	public final static UUID UUID_READ_CHARACTERISTIC = UUID
			.fromString("49535343-1e4d-4bd9-ba61-23c647249616");
	public final static UUID UUID_WRITE_CHARACTERISTIC = UUID
			.fromString("49535343-8841-43f4-a8d4-ecbe34729bb3");
	public static final String CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb";

	private Context mContext;
	private BluetoothAdapter mBluetoothAdapter;
	private String mBluetoothDeviceAddress;
	private Handler mWorkHandler;
	private Handler mAlertHandler;
	private BluetoothGatt mBluetoothGatt;
	private BluetoothGattCharacteristic notifyCharacteristic;
	private BluetoothGattCharacteristic writeCharacteristic = null;
	private OnConnectListener mOnConnectListener;
	private OnDataAvailableListener mOnDataAvailableListener;
	private AtomicInteger mConnectStatus = new AtomicInteger(BluetoothGatt.STATE_DISCONNECTED);
	private AtomicBoolean mIsStartService = new AtomicBoolean(false);
	private AtomicLong mDisconnectTime = new AtomicLong(SystemClock.elapsedRealtime());
	private AtomicLong mConnectTime = new AtomicLong(SystemClock.elapsedRealtime());

	private BluetoothGatt getBluetoothGatt() {
		return mBluetoothGatt;
	}

	private void setBluetoothGatt(BluetoothGatt bluetoothGatt) {
		this.mBluetoothGatt = bluetoothGatt;
	}

	private void setOnConnectListener(OnConnectListener l) {
		mOnConnectListener = l;
	}

	/**
	 * 分别监听连接状态／服务／读取／写入
	 */
	public void setOnDataAvailableListener(OnDataAvailableListener l) {
		mOnDataAvailableListener = l;
	}

	private OnConnectListener getOnConnectListener() {
		return mOnConnectListener;
	}

	public OnDataAvailableListener getOnDataAvailableListener() {
		return mOnDataAvailableListener;
	}

	BluetoothLeConnector(Context c, BluetoothAdapter adapter, String mac, Handler worker) {
		mContext = c.getApplicationContext();
		mBluetoothAdapter = adapter;
		mBluetoothDeviceAddress = mac;

		mWorkHandler = worker;

		HandlerThread thread = new HandlerThread("bluetooth alerter");
		thread.start();
		mAlertHandler = new Handler(thread.getLooper());
	}

	/**
	 * Implements callback methods for GATT events that the app cares about. For
	 * example, connection change and services discovered.
	 * GATT连接的各种监听回调方法
	 */
	private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
		@Override
		public void onConnectionStateChange(final BluetoothGatt gatt, final int status,
											final int newState) {
			mWorkHandler.post(new Runnable() {
				@Override
				public void run() {
					JLogKit.getInstance().i("onConnectionStateChange: thread "
							+ Thread.currentThread() + " status " + status
							+ " newState = " + newState);

					// 清空连接初始化的超时连接任务代码
					mAlertHandler.removeCallbacksAndMessages(null);

					if (status != BluetoothGatt.GATT_SUCCESS) {
						String err = "Cannot connect device with error status: " + status;
						disconnectGatt();
						JLogKit.getInstance().e(err);
						mOnConnectListener.onError(err);
						mConnectStatus.set(BluetoothGatt.STATE_DISCONNECTED);
						return;
					}

					if (newState == BluetoothProfile.STATE_CONNECTED) {
						// setting connect status is connected
						mConnectStatus.set(BluetoothGatt.STATE_CONNECTED);
						mOnConnectListener.onConnect();

						// Attempts to discover services after successful connection.
						mIsStartService.set(false);
						if (!gatt.discoverServices()) {
							String err = "discover service return false";
							JLogKit.getInstance().e(err);
							gatt.disconnect();
							mOnConnectListener.onError(err);
							return;
						}

						// 解决连接 Service 过长的问题
						// 有些手机第一次启动服务的时间大于 2s
						mAlertHandler.postDelayed(new Runnable() {
							@Override
							public void run() {
								mWorkHandler.post(new Runnable() {
									@Override
									public void run() {
										if (!mIsStartService.get()) {
											gatt.disconnect();
										}
									}
								});
							}
						}, 3000L);

					} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {

						if (!mIsStartService.get()) {
							String err = "service not found force disconnect";
							JLogKit.getInstance().e(err);
							mOnConnectListener.onError(err);
						}

						mOnConnectListener.onDisconnect();
						close();
						mConnectStatus.set(BluetoothGatt.STATE_DISCONNECTED);
					}
				}
			});
		}

		@Override
		public void onServicesDiscovered(final BluetoothGatt gatt, final int status) {
			mWorkHandler.post(new Runnable() {
				@Override
				public void run() {
					// 清空连接服务设置的超时回调
					mIsStartService.set(true);
					mAlertHandler.removeCallbacksAndMessages(null);

					if (status == BluetoothGatt.GATT_SUCCESS) {
						JLogKit.getInstance().i("onServicesDiscovered in thread " + Thread.currentThread());
						startReceive();
						mOnConnectListener.onServiceDiscover();
					} else {
						String err = "onServicesDiscovered received: " + status;
						JLogKit.getInstance().e(err);
						gatt.disconnect();
					}
				}
			});
		}

		@Override
		public void onCharacteristicRead(final BluetoothGatt gatt,
										 final BluetoothGattCharacteristic characteristic,
										 final int status) {

			JLogKit.getInstance().i("callback characteristic read status " + status
					+ " in thread " + Thread.currentThread());
			if (status == BluetoothGatt.GATT_SUCCESS && mOnDataAvailableListener != null) {
				mOnDataAvailableListener.onCharacteristicRead(
						characteristic.getValue(),
						status);
			}

		}

		@Override
		public void onCharacteristicChanged(final BluetoothGatt gatt,
											final BluetoothGattCharacteristic characteristic) {

			//JLogKit.getInstance().i("callback characteristic change in thread " + Thread.currentThread());
			if (mOnDataAvailableListener != null) {
				mOnDataAvailableListener.onCharacteristicChange(
						characteristic.getUuid(), characteristic.getValue());
			}
		}

		@Override
		public void onCharacteristicWrite(final BluetoothGatt gatt,
										  final BluetoothGattCharacteristic characteristic,
										  final int status) {
			JLogKit.getInstance().i("callback characteristic write in thread " + Thread.currentThread());
			if (mOnDataAvailableListener != null) {
				mOnDataAvailableListener.onCharacteristicWrite(
						characteristic.getUuid(), status);
			}
		}

		@Override
		public void onDescriptorWrite(final BluetoothGatt gatt,
									  final BluetoothGattDescriptor descriptor,
									  final int status) {
			JLogKit.getInstance().i("callback descriptor write in thread " + Thread.currentThread());

			if (mOnDataAvailableListener != null) {
				mOnDataAvailableListener.onDescriptorWrite(
						descriptor.getUuid(), status);
			}
		}
	};

	private boolean startReceive() {
		BluetoothGattService service = mBluetoothGatt.getService(UUID_SERVICE_CHANNEL);
		if (service == null)
			return false;
		BluetoothGattCharacteristic readCharacteristic = service.getCharacteristic(UUID_READ_CHARACTERISTIC);
		writeCharacteristic = service.getCharacteristic(UUID_WRITE_CHARACTERISTIC);
		int charaProp = readCharacteristic.getProperties();
		JLogKit.getInstance().i("charaProp = " + charaProp);
		if ((charaProp & BluetoothGattCharacteristic.PROPERTY_READ) > 0) {
			JLogKit.getInstance().i("(charaProp & BluetoothGattCharacteristic.PROPERTY_READ) > 0");
			if (notifyCharacteristic != null) {
				setCharacteristicNotification(notifyCharacteristic, false);
				notifyCharacteristic = null;
			}
			readCharacteristic(readCharacteristic);
		}
		if ((charaProp | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
			JLogKit.getInstance().i("enable notification");
			notifyCharacteristic = readCharacteristic;
			setCharacteristicNotification(readCharacteristic, true);
			return true;
		}
		return false;
	}


	/**
	 * Connects to the GATT server hosted on the Bluetooth LE device.
	 */
	public void connect(final OnConnectListener callback) {
		mWorkHandler.post(new Runnable() {
			@Override
			public void run() {
				JLogKit.getInstance().i("connect: in thread " + Thread.currentThread());

				if (mBluetoothAdapter == null) {
					String err = "BluetoothAdapter not initialized or unspecified address.";
					JLogKit.getInstance().e(err);
					callback.onError(err);
					return;
				}

				final BluetoothDevice device
						= mBluetoothAdapter.getRemoteDevice(mBluetoothDeviceAddress);
				if (device == null) {
					String err = "Device not found. Unable to connect.";
					JLogKit.getInstance().e(err);
					callback.onError(err);
					return;
				}

				// 避免自动硬件断开后又自动连接，导致 service 回调被调用
				// 这里有隐患，实践证明 close 方法是异步调用的且单例，
				// 这就是说当一个 gatt 被创建之后，调用之前的 gatt 可能会把当前的 gatt close掉.
				// 最终造成 gatt 泄漏问题.
				// 一个解决方案就是延长连接硬件的时间
				if (mConnectStatus.get() != BluetoothGatt.STATE_DISCONNECTED) {
					String err = "Device is connecting";
					JLogKit.getInstance().e(err);
					callback.onError(err);
					return;
				}

				// 检查完没有任何错误再设置回调，确保上一次没有完成的操作得以继续回调，而不是被新的回调覆盖
				setOnConnectListener(callback);

				// We want to directly connect to the device, so we are setting the
				// autoConnect
				// parameter to false.
				JLogKit.getInstance().i("Trying to create a new connection.");
				mConnectTime.set(SystemClock.elapsedRealtime());
				setBluetoothGatt(device.connectGatt(mContext, false, mGattCallback));
				if (getBluetoothGatt() == null) {
					String err = "bluetooth is not open!";
					JLogKit.getInstance().e(err);
					callback.onError(err);
					return;
				}

				mConnectStatus.set(BluetoothGatt.STATE_CONNECTING);
				mIsStartService.set(false);

				// 开一个定时器，如果超出 20s 就强制断开连接
				// 这个定时器必须在连接上设备之后清掉
				mAlertHandler.removeCallbacksAndMessages(null);
				mAlertHandler.postDelayed(new Runnable() {
					@Override
					public void run() {
						mWorkHandler.post(new Runnable() {
							@Override
							public void run() {
								if (mConnectStatus.get() == BluetoothGatt.STATE_CONNECTING) {
									disconnectGatt();
									String err = "connect timeout, cannot not connect device";
									JLogKit.getInstance().e(err);
									callback.onError(err);
								}
							}
						});
					}
				}, 20000L);
			}
		});
	}

	/**
	 * Disconnects an existing connection or cancel a pending connection. The
	 * disconnection result is reported asynchronously through the
	 * {@link BluetoothGattCallback#onConnectionStateChange(BluetoothGatt, int, int)}
	 * callback.
	 */
	public void disconnect() {
		mWorkHandler.post(new Runnable() {
			@Override
			public void run() {
				disconnectGatt();
			}
		});
	}

	private void disconnectGatt() {
		JLogKit.getInstance().i("disconnect: in thread " + Thread.currentThread());

		if (mBluetoothAdapter == null || getBluetoothGatt() == null) {
			JLogKit.getInstance().e("BluetoothAdapter not initialized");
			return;
		}

		if (mConnectStatus.get() == BluetoothGatt.STATE_DISCONNECTED) {
			close();
			return;
		}

		getBluetoothGatt().disconnect();

		// 确保 Gatt 一定会被 close
		if (mConnectStatus.get() == BluetoothGatt.STATE_CONNECTING) {
			mAlertHandler.removeCallbacksAndMessages(null);
			close();
		}
	}

	/**
	 * After using a given BLE device, the app must call this method to ensure
	 * resources are released properly.
	 */
	private void close() {
		JLogKit.getInstance().i("close: in thread " + Thread.currentThread());

		if (getBluetoothGatt() == null) {
			JLogKit.getInstance().e("BluetoothAdapter not initialized");
			return;
		}

		mDisconnectTime.set(SystemClock.elapsedRealtime());
		getBluetoothGatt().close();
		setBluetoothGatt(null);
		mConnectStatus.set(BluetoothGatt.STATE_DISCONNECTED);
	}

	/**
	 * Request a read on a given {@code BluetoothGattCharacteristic}. The read
	 * result is reported asynchronously through the
	 * {@code BluetoothGattCallback#onCharacteristicRead(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattCharacteristic, int)}
	 * callback.
	 * 从蓝牙模块读取数据, 读取的数据将会异步回调到
	 * {@link BluetoothLeConnector#setOnDataAvailableListener(OnDataAvailableListener)}
	 * 方法设置的监听中
	 *
	 * @param characteristic The characteristic to read from.
	 */
	public void readCharacteristic(BluetoothGattCharacteristic characteristic) {
		if (mBluetoothGatt == null) {
			JLogKit.getInstance().e("BluetoothAdapter not initialized");
			return;
		}
		mBluetoothGatt.readCharacteristic(characteristic);
	}


	/**
	 * 4.0的发送数据
	 *
	 * @param buffer 数据
	 * @return 发送成功与否
	 */
	public boolean sendCommand(byte[] buffer) {
		if (mBluetoothGatt == null || writeCharacteristic == null) {
			JLogKit.getInstance().i("btGatt == null || writeCharacteristic == null");
			return false;
		}
		writeCharacteristic.setValue(buffer);
		boolean bll = mBluetoothGatt.writeCharacteristic(writeCharacteristic);
		return bll;
	}

	/**
	 * Enables or disables notification on a give characteristic.
	 * 设置获取特征值UUID通知
	 *
	 * @param characteristic Characteristic to act on.
	 * @param enabled        If true, enable notification. False otherwise.
	 */
	public void setCharacteristicNotification(
			BluetoothGattCharacteristic characteristic, boolean enabled) {
		if (mBluetoothGatt == null) {
			JLogKit.getInstance().e("BluetoothAdapter not initialized");
			return;
		}
		mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
		BluetoothGattDescriptor descriptor = characteristic.getDescriptor(UUID
				.fromString(CLIENT_CHARACTERISTIC_CONFIG));
		if (descriptor != null) {
			JLogKit.getInstance().i("write descriptor");
			descriptor
					.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
			mBluetoothGatt.writeDescriptor(descriptor);
		}
	}
}
